﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace KataBankOCR
{
    /// <summary>
    /// Class to contain a single account number stored in string format. 
    /// It may contain '?' where digits could not be decoded from scan
    /// 
    /// </summary>
    class AccountNumber
    {
        public const string okStatus = "";
        public const string errorStatus = "ERR";
        public const string illegalCharsStatus = "ILL";
        public const string ambiguousStatus = "AMB";
        public const int noErrorCorrection = 0;
        public const int oneDigitErrorCorrection = 1;
        public const int multiDigitErrorCorrection = 2;

        /// <summary>
        /// This is the max number of digits that can appear on a logical "line"
        /// </summary>
        public const int digitsInAccountNumber=9;


        private string status = "";
        private string digitString;
        private List<string> otherPossibleAccountNumbers = new List<string>();
        private int errCorrectionLevel;

        public int ErrCorrectionLevel
        {
            get { return errCorrectionLevel; }
            set { errCorrectionLevel = value; }
        }

        public List<string> OtherPossibleAccountNumbers
        {
            get { return otherPossibleAccountNumbers; }
            set { otherPossibleAccountNumbers = value; }
        }

        public string Status
        {
            get { return status; }
            set { status = value; }
        }

        public string DigitString
        {
            get { return digitString; }
        }

        /// <summary>
        /// This function calculates the summed portion of the checksum recursively. 
        /// </summary>
        /// <param name="digitIndex">Index of digit in digitString to calc sum for. Initial call use 0</param>
        /// <param name="multiplier">int to multiply digit by. Initial all use digitsInAccountNumber</param>
        /// <returns>Calculated chksum for all digit values after this one</returns>
        private Int64 calcSum(string stringToCheck, int digitIndex, int multiplier)
        {
            Int64 digitValue = Int64.Parse(stringToCheck.Substring(digitIndex,1));
            if (multiplier == 1) 
            {
                // last digit so stop recursion and return its value
                return digitValue;
            }
            else
            {
                // all other digits 
                return (multiplier * digitValue) + calcSum(stringToCheck, ++digitIndex, --multiplier);
            }
        }


        private Boolean checkSumOk(string stringToCheck)
        {
            //Int64 eleven = 11;
            return ((calcSum(stringToCheck,0, digitsInAccountNumber) % 11)== 0);
        }

        /// <summary>
        /// Search lines for valid account number(s) doing error correction.
        /// </summary>
        /// <param name="accountNumStrs">Scanned lines of Digit</param>
        /// <param name="startIndex">Index into a line to start reading character</param>
        /// <param name="accBuildStr">Target digit to check for as built so far</param>
        /// <param name="targetDigit">Digit number to do error correction on 0-digitsInAccountNumber. 
        /// Or if less than 0 try to correct in any and all digits</param>
        private void addErrorCorrectedAccountNumbers(string[] accountNumStrs,
                                                   int startIndex,
                                                   StringBuilder accBuildStr,
                                                   int targetDigit)
        {
            if (accBuildStr.Length < digitsInAccountNumber)
            {
                try
                {
                    if (accBuildStr.Length == targetDigit || targetDigit <0)
                    {
                        foreach (char c in Digit.oneBarAdjustDecode(accountNumStrs, startIndex))
                        {
                            accBuildStr.Append(c);  // this case digit
                            addErrorCorrectedAccountNumbers(accountNumStrs, startIndex + Digit.numChars, accBuildStr, targetDigit);
                            accBuildStr.Remove(accBuildStr.Length - 1, 1); //remove this case digit
                        }
                    }
                    else
                    {
                        accBuildStr.Append(Digit.decodeDigit(accountNumStrs, startIndex));
                        addErrorCorrectedAccountNumbers(accountNumStrs, startIndex + Digit.numChars, accBuildStr, targetDigit);
                        accBuildStr.Remove(accBuildStr.Length - 1, 1); //remove this case digit
                    }
                }
                catch (Exception e)
                {
                    Console.WriteLine("Could not parse digit " + (accBuildStr.Length).ToString() + " at index " + startIndex.ToString());
                    Console.WriteLine("'" + accountNumStrs[0] + "'");
                    Console.WriteLine("'" + accountNumStrs[1] + "'");
                    Console.WriteLine("'" + accountNumStrs[2] + "'");
                    Console.WriteLine("Got exception" + e.Message);
                    Console.WriteLine("Parsed this so far " + accBuildStr.ToString());
                    digitString = "BAD_PARSE";
                    status = errorStatus;
                    return;
                }
            }
            else
            {
                string localDigitString = accBuildStr.ToString();
                if (localDigitString.IndexOf('?') == -1)
                {
                    if (checkSumOk(localDigitString))
                    {
                        // valid account number so add to list
                        if (status == illegalCharsStatus)
                        {
                            // First valid account number during error correction, so store it
                            digitString = localDigitString;
                            status = okStatus;  // set status to show no error
                        }
                        else
                        {
                            // found more than one account number during error correction, so store others in list
                            otherPossibleAccountNumbers.Add(localDigitString);
                            status = ambiguousStatus;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Constructor to initialize a account number from scanned form (i.e. multiple lines)
        /// </summary>
        /// <param name="accountNumStrs">Scanned lines of Digit</param>
        /// <param name="errorCorrectionLevel">0= no correction, 1=Only one digit in account, 2=Correct multiple digits</param>
        public AccountNumber(string[] accountNumStrs, int errorCorrectionLevel)
        {
            errCorrectionLevel = errorCorrectionLevel;

            // parse digits from account num lines
            StringBuilder accountnumberStr = new StringBuilder();

            
            for (int digitCount = 0, startIndex = 0;
                 digitCount < AccountNumber.digitsInAccountNumber;
                 digitCount++, startIndex += Digit.numChars)
            {
                try
                {
                    accountnumberStr.Append(Digit.decodeDigit(accountNumStrs, startIndex));
                }
                catch (Exception e)
                {
                    Console.WriteLine("Could not parse digit " + (digitCount + 1).ToString() + " at index " + startIndex.ToString());
                    Console.WriteLine("'" + accountNumStrs[0] + "'");
                    Console.WriteLine("'" + accountNumStrs[1] + "'");
                    Console.WriteLine("'" + accountNumStrs[2] + "'");
                    Console.WriteLine("Got exception" + e.Message);
                    Console.WriteLine("Parsed this so far " + accountnumberStr.ToString());
                    digitString = "BAD_PARSE";
                    status = errorStatus;
                    return;
                }
            }

            digitString = accountnumberStr.ToString();
            if (digitString.IndexOf('?') > -1)
            {
                status = illegalCharsStatus;
            }
            else
            {
                if (!checkSumOk(digitString))
                {
                    status = errorStatus;
                }
            }
            if (status != okStatus && errorCorrectionLevel > noErrorCorrection)
            {
                // had problem so try error correction guesses to find valid account numbers
                accountnumberStr = new StringBuilder();
                if (errorCorrectionLevel == oneDigitErrorCorrection)
                {
                    for (int targetDigit = 0; targetDigit < digitsInAccountNumber; targetDigit++)
                    {
                        addErrorCorrectedAccountNumbers(accountNumStrs, 0, accountnumberStr, targetDigit);
                    }
                }
                else
                {
                    if (errorCorrectionLevel==multiDigitErrorCorrection)
                    {
                        // pass target digit of -1 so multiple digits corrected.
                        addErrorCorrectedAccountNumbers(accountNumStrs, 0, accountnumberStr, -1);
                    }
                }
                if (status == ambiguousStatus && otherPossibleAccountNumbers.Count == 1)
                {
                    // only one number found so make it the only reported
                    digitString = otherPossibleAccountNumbers[0];
                    otherPossibleAccountNumbers = new List<string>();
                    status = okStatus;
                }

            }
        }

        /// <summary>
        /// Overriden ToString method
        /// </summary>
        /// <returns>Account Number in string form. May contain '?' where digits could not be decoded from scan</returns>
        public override string ToString()
        {
            StringBuilder returnStr = new StringBuilder(digitString);
            if (errCorrectionLevel != noErrorCorrection)
            {
                returnStr.Append(" " + status);
                if (otherPossibleAccountNumbers.Count > 0)
                {
                    returnStr.Append(" [");
                    for (int otherIndex = 0; otherIndex < otherPossibleAccountNumbers.Count; otherIndex++)
                    {
                        returnStr.Append("'" + otherPossibleAccountNumbers[otherIndex] + "'");
                        if (otherIndex < otherPossibleAccountNumbers.Count - 1)
                        {
                            returnStr.Append(",");
                        }
                    }
                    returnStr.Append("]");
                }
            }
            return returnStr.ToString();
        }
    }
}
